package com.king.armips.android.widget;

import android.animation.ObjectAnimator;
import android.animation.ValueAnimator;
import android.annotation.SuppressLint;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.graphics.Typeface;
import android.graphics.drawable.ColorDrawable;
import android.text.Layout;
import android.util.AttributeSet;
import android.util.Log;
import android.util.Range;
import android.view.GestureDetector;
import android.view.Gravity;
import android.view.LayoutInflater;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.OverScroller;
import android.widget.PopupWindow;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.king.armips.android.R;
import com.king.armips.android.utils.ScreenUtils;

import java.io.File;
import java.io.IOException;
import java.io.RandomAccessFile;
import java.math.BigInteger;
import java.util.ArrayList;
import java.util.List;

import io.github.rosemoe.editor.util.ClipboardUtils;

public class HexDataView extends View {

    private static final String TAG = HexDataView.class.getSimpleName();
    private static final int TEXT_COLOR_DATA_OFFSET = 0xFF555555;
    private static final int TEXT_COLOR_DATA = 0xFF000000;

    private Context mContext;
    private Paint mTextPaint;
    private Paint mSelectedBlockPaint;

    private RandomAccessFile mSrcFileReader = null;
    private int mScrollY = 0;
    private float mLineHeight;
    private float mFontTopSpace;
    private float mSizeUnit;

    private final GestureDetector mGestureDetector;
    private final OverScroller mFlingScroller;
    private ValueAnimator mFlingAnimator;

    private boolean mDataOffsetOver = false;

    private int mSelectedStartOffset = -1;
    private int mSelectedEndOffset = -1;
    private List<RectF> mSelectedRectList = new ArrayList<>();
    private RectF mSelectedStartHandleRect = null;
    private RectF mSelectedEndHandleRect = null;
    private int mTouchSelectedStartOffset;
    private int mTouchSelectedEndOffset;
    private boolean isScrolled = false;

    private PopupWindow mOperateWindow;

    private ClipboardUtils mClipboardUtils = new ClipboardUtils(this);

    public HexDataView(Context context) {
        this(context,null);
    }

    public HexDataView(Context context, @Nullable AttributeSet attrs) {
        this(context, attrs,0);
    }

    public HexDataView(Context context, @Nullable AttributeSet attrs, int defStyleAttr) {
        this(context, attrs, defStyleAttr,0);
    }

    public HexDataView(Context context, @Nullable AttributeSet attrs, int defStyleAttr, int defStyleRes) {
        super(context, attrs, defStyleAttr, defStyleRes);

        mContext = context;

        mTextPaint = new Paint();
        //设置等宽字体
        mTextPaint.setTypeface(Typeface.createFromAsset(context.getAssets(), "droid_sans_mono.ttf"));
        //设置字体大小
        mTextPaint.setTextSize(ScreenUtils.dp2px(context,19));
        //获取1个字符的宽度
        mSizeUnit = mTextPaint.measureText("0");

        Paint.FontMetrics fm = mTextPaint.getFontMetrics();
        mLineHeight = fm.descent - fm.ascent;
        mFontTopSpace = mLineHeight - Math.abs(fm.ascent);

        mSelectedBlockPaint = new Paint();
        mSelectedBlockPaint.setColor(context.getColor(R.color.colorAccent));

        mGestureDetector = new GestureDetector(context, new GestureDetector.SimpleOnGestureListener(){
            private float wholeDistanceX = 0f;
            private float wholeDistanceY = 0f;
            @Override
            public boolean onDown(MotionEvent e) {
                abortFling();
                float x = e.getX();
                float y = e.getY();
                wholeDistanceX = 0f;
                wholeDistanceY = 0f;
                isScrolled = false;

                boolean isSelectedStartHandleTouch = mSelectedStartHandleRect != null && mSelectedStartHandleRect.contains(x, y);
                boolean isSelectedEndHandleTouch = mSelectedEndHandleRect != null && mSelectedEndHandleRect.contains(x, y);

                mTouchSelectedStartOffset = isSelectedStartHandleTouch ? mSelectedStartOffset : -1;
                mTouchSelectedEndOffset = isSelectedEndHandleTouch ? mSelectedEndOffset : -1;

                return true;
            }

            @Override
            public boolean onScroll(MotionEvent e1, MotionEvent e2, float distanceX, float distanceY) {
                wholeDistanceX += distanceX;
                wholeDistanceY += distanceY;
                isScrolled = true;

                displayOperateWindow(false);

                if (mTouchSelectedStartOffset != -1 || mTouchSelectedEndOffset != -1){
                    int distanceLine = (int) (-wholeDistanceY / mLineHeight);
                    int distanceColumn = (int) (-wholeDistanceX / (mSizeUnit * 3));

                    int targetStartOffset;
                    int targetEndOffset;

                    if (mTouchSelectedStartOffset != -1){
                        targetStartOffset = mTouchSelectedStartOffset+distanceLine*8+distanceColumn;
                        targetEndOffset = mSelectedEndOffset;
                    }
                    else {
                        targetStartOffset = mSelectedStartOffset;
                        targetEndOffset = mTouchSelectedEndOffset+distanceLine*8+distanceColumn;
                    }

                    if (targetStartOffset == targetEndOffset){
                        targetStartOffset = mSelectedStartOffset;
                        targetEndOffset = targetStartOffset+1;
                    }
                    else if (targetStartOffset > targetEndOffset){
                        int temp = targetStartOffset;
                        targetStartOffset = targetEndOffset;
                        targetEndOffset = temp;
                        temp = mTouchSelectedStartOffset;
                        mTouchSelectedStartOffset = mTouchSelectedEndOffset;
                        mTouchSelectedEndOffset = temp;
                    }

                    setSelectedRange(targetStartOffset,targetEndOffset);
                    return true;
                }


                return dispatchScroll(distanceY);
            }

            @Override
            public boolean onFling(MotionEvent e1, MotionEvent e2, float velocityX, float velocityY) {
                if (velocityY == 0 || mTouchSelectedStartOffset != -1 || mTouchSelectedEndOffset != -1)
                    return false;
                if (!canScrollVertically(velocityY < 0 ? -1 : 1))
                    return false;

                mFlingScroller.fling(0, mScrollY, 0, -Math.round(velocityY), 0, 0, 0, getScrollMaxY(), 0, 0);

                mFlingAnimator = ObjectAnimator.ofFloat(0, 1);
                mFlingAnimator.setDuration(500);
                mFlingAnimator.setRepeatCount(ValueAnimator.INFINITE);
                mFlingAnimator.addUpdateListener(animation -> {
                    if (mFlingScroller.isFinished()){
                        mFlingAnimator.cancel();
                        return;
                    }
                    if (mFlingScroller.computeScrollOffset()) {
                        int newY = mFlingScroller.getCurrY();
                        if (newY != mScrollY){
                            setScrollY(newY);
                        }
                    }
                });
                mFlingAnimator.start();

                return super.onFling(e1, e2, velocityX, velocityY);
            }

            @Override
            public void onLongPress(MotionEvent e) {
                if (mSelectedStartOffset != -1)
                    return;

                float x = e.getX();
                float y = e.getY();

                int line = (int) (y / mLineHeight);
                int column = Math.min(7,(int) (Math.max(0,x-mSizeUnit*8) / (mSizeUnit*3)));

                int offset = getDataOffset() + line * 8 + column;

                if (offset >= getFileSize()){
                    offset = 0;
                }

                setSelectedRange(offset,offset+1);

            }

            @Override
            public boolean onSingleTapUp(MotionEvent e) {
                if (wholeDistanceY != 0 || wholeDistanceX != 0)
                    return false;

                float x = e.getX();
                float y = e.getY();
                boolean isTouchSelectedRange = false;
                for (RectF rect : mSelectedRectList){
                    if (rect.contains(x,y)){
                        isTouchSelectedRange = true;
                        break;
                    }
                }

                if (isTouchSelectedRange){
                    displayOperateWindow(true);
                }
                else {
                    clearSelection();
                    displayOperateWindow(false);
                }

                return super.onSingleTapUp(e);
            }
        });

        mFlingScroller = new OverScroller(context);

    }

    private boolean dispatchScroll(float dy){
        int targetScrollY = (int) (mScrollY + dy);
        if (dy > 0){
            if (mDataOffsetOver)
                return false;
        }
        else {
            if (targetScrollY <= 0){
                targetScrollY = 0;
            }
        }

        if (targetScrollY == mScrollY)
            return false;

        setScrollY(targetScrollY);
        return true;
    }

    private void abortFling(){
        mFlingScroller.abortAnimation();
        if (mFlingAnimator != null && mFlingAnimator.isRunning()) {
            mFlingAnimator.cancel();
        }
    }

    public boolean setSrcFile(File file){
        mSrcFileReader = null;
        mDataOffsetOver = false;
        try {
            mSrcFileReader = new RandomAccessFile(file,"r");
        }
        catch (Exception e){
            e.printStackTrace();
        }
        post(()->{
            clearSelection();
            setScrollY(0);
            invalidate();
        });
        return mSrcFileReader != null;
    }

    public int getDataOffset(){
        if (mScrollY == 0 || mSrcFileReader == null)
            return 0;

        int line = (int) (mScrollY / mLineHeight);

        return line * 8;
    }

    public void setDataOffset(int offset){
        int scrollMaxY = getScrollMaxY();

        clearSelection();

        if (offset >= getFileSize()){
            setScrollY(scrollMaxY);
        }
        else {
            offset = offset / 8 * 8;
            int line = offset / 8;
            int scrollY = (int) Math.ceil(line * mLineHeight);

            setScrollY(Math.min(scrollMaxY,scrollY));
        }
    }

    public void setSelectedRange(int startOffset,int endOffset){
        if (startOffset == -1){
            startOffset = mSelectedStartOffset;
        }
        if (endOffset == -1){
            endOffset = mSelectedEndOffset;
        }

        int lower = Math.min(startOffset,endOffset);
        int upper = Math.max(startOffset,endOffset);
        startOffset = lower;
        endOffset = upper;

        if (startOffset < 0)
            startOffset = 0;

        int fileSize = getFileSize();
        if (endOffset > fileSize)
            endOffset = fileSize;

        if (startOffset == mSelectedStartOffset && endOffset == mSelectedEndOffset)
            return;

        if (startOffset ==  endOffset){
            mSelectedStartOffset = -1;
        }
        else {
            mSelectedStartOffset = startOffset;
            mSelectedEndOffset = endOffset;
        }
        
        postInvalidate();
    }

    public void setSelectedRange(@Nullable Range<Integer> range) {
        if (range == null){
            displayOperateWindow(false);
            setSelectedRange(0,0);
        }
        else {
            setSelectedRange(range.getLower(),range.getUpper());
        }
    }

    public void clearSelection(){
        setSelectedRange(null);
    }

    @Nullable
    public Range<Integer> getSelectedRange() {
        if (mSelectedStartOffset == -1 || (mSelectedStartOffset == mSelectedEndOffset))
            return null;
        return new Range<>(mSelectedStartOffset,mSelectedEndOffset);
    }

    public boolean copySelectedData(){
        Range<Integer> selectedRange = getSelectedRange();
        if (selectedRange == null)
            return false;

        int start = selectedRange.getLower();
        int end = selectedRange.getUpper();
        StringBuilder dataBuilder = new StringBuilder();
        byte[] buffer = new byte[8];
        seekFile(start);
        while (start < end){
            int read = readFile(buffer);
            if (read <= 0)
                break;
            for (int i=0;i<read;i++){
                if (start >= end)
                    break;
                dataBuilder.append(byteToHex(buffer[i]));
                start++;
            }
        }
        return mClipboardUtils.writeSysClipText(dataBuilder.toString());
    }

    @Override
    public void setScrollY(int value) {
        super.setScrollY(value);
        mScrollY = value;
    }

    public int getScrolledY() {
        return mScrollY;
    }

    public int getScrollMaxY(){
        int lineCount = (int) Math.ceil(getFileSize() / 8f);
        int visibleLineCount = (int) Math.ceil(getHeight() / mLineHeight);
        return (int) Math.max(0,mLineHeight * (lineCount-visibleLineCount/2f));
    }

    @Override
    public boolean canScrollVertically(int direction) {
        if (direction < 0){
            return mScrollY > 0;
        }
        else {
            return !mDataOffsetOver;
        }
    }

    @Override
    protected int computeVerticalScrollRange() {
        return getScrollMaxY();
    }

    @Override
    protected int computeVerticalScrollOffset() {
        return Math.max(0, Math.min(getScrollMaxY(), getScrolledY()));
    }

    @SuppressLint("ClickableViewAccessibility")
    @Override
    public boolean onTouchEvent(MotionEvent event) {
        boolean processed = mGestureDetector.onTouchEvent(event);

        switch (event.getAction()){
            case MotionEvent.ACTION_UP:
            case MotionEvent.ACTION_CANCEL:
                if (!isScrolled || mTouchSelectedStartOffset != -1 || mTouchSelectedEndOffset != -1) {
                    post(() -> displayOperateWindow(mSelectedStartOffset != -1));
                }
                break;
        }

        return processed;
    }

    @SuppressLint("DrawAllocation")
    @Override
    protected void onDraw(Canvas canvas) {
        super.onDraw(canvas);

        if (getFileSize() == 0)
            return;

        mSelectedRectList.clear();
        mSelectedStartHandleRect = null;
        mSelectedEndHandleRect = null;

        canvas.translate(0,mScrollY);

        float baseX = mSizeUnit / 2f;
        float baseY = -(mScrollY % mLineHeight)+mLineHeight;
        float dataOffsetTextWidth = baseX + mSizeUnit * 8;

        int lineCount = (int) Math.ceil(getHeight() / mLineHeight)+1;

        byte[] buffer = new byte[8];
        int dataOffset = getDataOffset();
        int line = 0;

        seekFile(dataOffset);

        do {
            int read = readFile(buffer);
            if (read <= 0)
                break;

            float y = line * mLineHeight + baseY;

            mTextPaint.setColor(TEXT_COLOR_DATA_OFFSET);
            int lineOffset = dataOffset+line*8;
            String offsetText = String.format("%08X",lineOffset);
            canvas.drawText(offsetText,baseX,y,mTextPaint);

            StringBuilder dataStrBuilder = new StringBuilder();
            int startSelectedColumn = -1;
            int endSelectedColumn = -1;
            for (int i=0;i<read;i++){
                dataStrBuilder.append(" ");
                dataStrBuilder.append(byteToHex(buffer[i]));

                if (mSelectedStartOffset != -1) {
                    int offset = lineOffset + i;
                    if (mSelectedStartOffset <= offset && offset < mSelectedEndOffset) {
                        if (startSelectedColumn == -1) {
                            startSelectedColumn = i;
                        }
                        endSelectedColumn = i+1;
                    }
                }

            }

            if (startSelectedColumn != -1){
                mSelectedBlockPaint.setAlpha((int) (255 * 0.5f));
                RectF blockRect = new RectF(
                        dataOffsetTextWidth + startSelectedColumn * mSizeUnit * 3 + baseX,
                        y - mLineHeight + mFontTopSpace,
                        dataOffsetTextWidth + endSelectedColumn * mSizeUnit * 3 + baseX,
                        y + mFontTopSpace
                );

                mSelectedRectList.add(blockRect);
                canvas.drawRect(blockRect,mSelectedBlockPaint);

                if (mSelectedStartOffset == startSelectedColumn + lineOffset){
                    mSelectedBlockPaint.setAlpha(255);
                    float radius = mLineHeight / 2;
                    float cx = dataOffsetTextWidth + startSelectedColumn * mSizeUnit * 3 + baseX - radius;
                    float cy = y + mFontTopSpace + radius;

                    mSelectedStartHandleRect = new RectF(
                            cx-radius,
                            cy-radius,
                            cx+radius,
                            cy+radius
                    );
                }

                if (mSelectedEndOffset == endSelectedColumn + lineOffset){
                    mSelectedBlockPaint.setAlpha(255);
                    float radius = mLineHeight / 2;
                    float cx = dataOffsetTextWidth + endSelectedColumn * mSizeUnit * 3 + baseX + radius;
                    float cy = y + mFontTopSpace + radius;

                    mSelectedEndHandleRect = new RectF(
                            cx-radius,
                            cy-radius,
                            cx+radius,
                            cy+radius
                    );
                }

            }

            mTextPaint.setColor(TEXT_COLOR_DATA);
            canvas.drawText(dataStrBuilder.toString(),dataOffsetTextWidth,y,mTextPaint);

            line++;

        }while (line < lineCount);

        if (mSelectedStartHandleRect != null){
            mSelectedBlockPaint.setAlpha(255);
            float radius = mLineHeight / 2;

            canvas.drawCircle(mSelectedStartHandleRect.left+radius,mSelectedStartHandleRect.top+radius,radius,mSelectedBlockPaint);
            canvas.drawRect(mSelectedStartHandleRect.left+radius, mSelectedStartHandleRect.top, mSelectedStartHandleRect.right, mSelectedStartHandleRect.top+radius, mSelectedBlockPaint);

        }

        if (mSelectedEndHandleRect != null){
            mSelectedBlockPaint.setAlpha(255);
            float radius = mLineHeight / 2;

            canvas.drawCircle(mSelectedEndHandleRect.left+radius,mSelectedEndHandleRect.top+radius,radius,mSelectedBlockPaint);
            canvas.drawRect(mSelectedEndHandleRect.left, mSelectedEndHandleRect.top, mSelectedEndHandleRect.left+radius, mSelectedEndHandleRect.top+radius, mSelectedBlockPaint);
        }

        mDataOffsetOver = line <= Math.ceil(lineCount / 2f);

    }

    @Override
    protected void onVisibilityChanged(@NonNull View changedView, int visibility) {
        super.onVisibilityChanged(changedView, visibility);
        post(() -> {
            displayOperateWindow(false);
        });
    }

    private void seekFile(long offset){
        try {
            mSrcFileReader.seek(offset);
        } catch (IOException ignored) { }
    }

    private int readFile(byte[] buffer){
        int read = -1;
        try {
            read = mSrcFileReader.read(buffer);
        } catch (IOException ignored) { }
        return read;
    }

    private void displayOperateWindow(boolean show){
        if (show){
            if (mOperateWindow == null) {
                mOperateWindow = new PopupWindow(mContext);
                mOperateWindow.setBackgroundDrawable(new ColorDrawable(Color.TRANSPARENT));
                mOperateWindow.setWidth(ViewGroup.LayoutParams.WRAP_CONTENT);
                mOperateWindow.setHeight(ViewGroup.LayoutParams.WRAP_CONTENT);
                View popView = LayoutInflater.from(mContext).inflate(R.layout.pop_hex_data_options, null);
                mOperateWindow.setContentView(popView);

                popView.findViewById(R.id.item_btn_copy).setOnClickListener(v -> {
                    mOperateWindow.dismiss();
                    if (copySelectedData()) {
                        Toast.makeText(mContext, R.string.copy_success, Toast.LENGTH_SHORT).show();
                    } else {
                        Toast.makeText(mContext, R.string.copy_failed, Toast.LENGTH_SHORT).show();
                    }
                });

                popView.measure(0,0);
            }

            int[] location = new int[2];
            getLocationInWindow(location);

            RectF handleRect = null;
            if (mTouchSelectedStartOffset != -1){
                handleRect = mSelectedStartHandleRect;
            }
            else if (mTouchSelectedEndOffset != -1){
                handleRect = mSelectedEndHandleRect;
            }
            if (handleRect == null){
                if (mSelectedStartHandleRect != null){
                    handleRect = mSelectedStartHandleRect;
                }
                else if (mSelectedEndHandleRect != null){
                    handleRect = mSelectedEndHandleRect;
                }
            }

            int x = (getWidth() - mOperateWindow.getContentView().getMeasuredWidth()) / 2 + location[0];
            int y = 0;

            if (handleRect != null){
                int popHeight = mOperateWindow.getContentView().getMeasuredHeight();
                if (handleRect.top - popHeight - mLineHeight > 0){
                    y = (int) (handleRect.top - popHeight - mLineHeight);
                }
                else {
                    y = (int) (handleRect.bottom);
                }
            }
            y += location[1];

            if (mOperateWindow.isShowing()){
                mOperateWindow.update(x,y,-1,-1);
            }
            else {
                mOperateWindow.showAtLocation(this, Gravity.NO_GRAVITY,x,y);
            }

        }
        else {
            if (mOperateWindow != null && mOperateWindow.isShowing()){
                mOperateWindow.dismiss();
            }
        }
    }

    public int getFileSize(){
        if (mSrcFileReader == null)
            return 0;
        long size = 0;
        try {
            size = mSrcFileReader.length();
        } catch (IOException ignored) { }
        return BigInteger.valueOf(size).intValue();
    }

    private String byteToHex(byte b){
        return String.format("%02X",b&0xFF);
    }


}
